---
title: How to debounce/cancel network requests
version: 1
---

import { Link } from "/src/components/Link";
import { AutoSnippet, When } from "/src/components/CodeSnippet";
import homeScreen from "!raw-loader!./cancel/home_screen.dart";
import extension from "!raw-loader!./cancel/extension.dart";
import detailScreen from "./cancel/detail_screen";
import detailScreenCancel from "./cancel/detail_screen_cancel";
import detailScreenDebounce from "./cancel/detail_screen_debounce";
import providerWithExtension from "./cancel/provider_with_extension";

As applications grow in complexity, it's common to have multiple network requests
in flight at the same time. For example, a user might be typing in a search box
and triggering a new request for each keystroke. If the user types quickly, the
application might have many requests in flight at the same time.

Alternatively, a user might trigger a request, then navigate to a different page
before the request completes. In this case, the application might have a request
in flight that is no longer needed.

To optimize performance in those situations, there are a few techniques you can
use:

- "Debouncing" requests. This means that you wait until the user has stopped
  typing for a certain amount of time before sending the request. This ensures
  that you only send one request for a given input, even if the user types
  quickly.
- "Cancelling" requests. This means that you cancel a request if the user
  navigates away from the page before the request completes. This ensures that
  you don't waste time processing a response that the user will never see.

In Riverpod, both of these techniques can be implemented in a similar way.
The key is to use `ref.onDispose` combined with "automatic disposal" or `ref.watch`
to achieve the desired behavior.

To showcase this, we will make a simple application with two pages:

- A home screen, with a button which opens a new page
- A detail page, which displays a random activity from the [Bored API](https://www.boredapi.com/),
  with the ability to refresh the activity.  
  See <Link documentID="how_to/pull_to_refresh" /> for information
  on how to implement pull-to-refresh.

We will then implement the following behaviors:

- If the user opens the detail page and then navigates back immediately,
  we will cancel the request for the activity.
- If the user refreshes the activity multiple times in a row, we will debounce
  the requests so that we only send one request after the user stops refreshing.

## The application

<img
  src="/img/how_to/cancel/app.gif"
  alt="Gif showcasing the application, opening the detail page and refreshing the activity."
/>

First, let's create the application, without any debouncing or cancelling.  
We won't use anything fancy here, and stick to a plain `FloatingActionButton` with
a `Navigator.push` to open the detail page.

First, let's start with defining our home screen. As usual,
let's not forget to specify a `ProviderScope` at the root of our application.

<AutoSnippet title="lib/src/main.dart" raw={homeScreen} />

Then, let's define our detail page.
To fetch the activity and implement pull-to-refresh, refer
to the <Link documentID="how_to/pull_to_refresh" /> case study.

<AutoSnippet title="lib/src/detail_screen.dart" {...detailScreen} />

## Cancelling requests

Now that we have a working application, let's implement the cancellation logic.

To do so, we will use `ref.onDispose` to cancel the request when the user
navigates away from the page. For this to work, it is important that the
automatic disposal of providers is enabled.

The exact code needed to cancel the request will depend on the HTTP client.
In this example, we will use `package:http`, but the same principle applies
to other clients.

The key here is that `ref.onDispose` will be called when the user navigates away.
That is because our provider is no-longer used, and therefore disposed
thanks to automatic disposal.  
We can therefore use this callback to cancel the request. When using `package:http`,
this can be done by closing our HTTP client.

<AutoSnippet {...detailScreenCancel} />

## Debouncing requests

Now that we have implemented cancellation, let's implement debouncing.  
At the moment, if the user refreshes the activity multiple times in a row,
we will send a request for each refresh.

Technically speaking, now that we have implemented cancellation, this is not
a problem. If the user refreshes the activity multiple times in a row,
the previous request will be cancelled, when a new request is made.

However, this is not ideal. We are still sending multiple requests, and
wasting bandwidth and server resources.  
What we could instead do is delay our requests until the user stops refreshing
the activity for a fixed amount of time.

The logic here is very similar to the cancellation logic. We will again
use `ref.onDispose`. However, the idea here is that instead of
closing an HTTP client, we will rely on `onDispose` to abort the request
before it starts.  
We will then arbitrarily wait for 500ms before sending the request.
Then, if the user refreshes the activity again before the 500ms have elapsed,
`onDispose` will be invoked, aborting the request.

:::info
To abort requests, a common practice is to voluntarily throw.  
It is safe to throw inside providers after the provider has been disposed.
The exception will naturally be caught by Riverpod and be ignored.
:::

<AutoSnippet {...detailScreenDebounce} />

## Going further: Doing both at once

We now know how to debounce and cancel requests.  
But currently, if we want to do another request, we need to copy-paste
the same logic in multiple places. This is not ideal.

However, we can go further and implement a reusable utility to do both at once.

The idea here is to implement an extension method on `Ref` that will
handle both cancellation and debouncing in a single method.

<AutoSnippet raw={extension} />

We can then use this extension method in our providers as followed:

<AutoSnippet {...providerWithExtension} />
